# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import base64
import hashlib
import hmac
import json
import logging
import time
import urllib.parse

import requests
import requests.auth

import coin.exchange.base.kr_rest.rest_client_base as rcb
from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.fcoin.kr_rest.constants import fcoin_order_status_list
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.exchange.util.rate_limit import RateLimiter


def _remove_none_and_sort_params(params):
  params = params or {}
  params = {key: value for key, value in params.items() if value is not None}
  sorted_params = sorted(params.items(), key=lambda d: d[0])
  return sorted_params


FCOIN_API_URL = "https://api.fcoin.com/v2/"


class FcoinAuth(requests.auth.AuthBase):
  def __init__(self, key_file):
    key = AuthKey.from_file(key_file=key_file)
    self._key = key

  def _get_sign(self, http_method, url, params, timestamp):
    assert http_method in ('GET', 'POST'), http_method
    encoded_params = urllib.parse.urlencode(params)
    if http_method == 'GET':
      if encoded_params:
        url = '%s?%s' % (url, encoded_params)
      sign_string = "%s%s%s" % (http_method, url, timestamp)
    elif http_method == 'POST':
      sign_string = "%s%s%s%s" % (http_method, url, timestamp, encoded_params)
    else:
      raise ValueError('Unknown http method %s' % http_method)

    signature = hmac.new(self._key.secret_key.encode(),
                         base64.b64encode(sign_string.encode()),
                         digestmod=hashlib.sha1)

    signature = base64.b64encode(signature.digest())
    return signature.decode()

  def _sign_header(self, http_method, url, params):
    timestamp = int(time.time() * 1000)
    header = {
        'FC-ACCESS-KEY': self._key.access_key,
        'FC-ACCESS-SIGNATURE': self._get_sign(http_method, url, params, timestamp),
        'FC-ACCESS-TIMESTAMP': str(timestamp)
    }
    return header

  def __call__(self, req: requests.PreparedRequest):
    params = None
    if req.body is not None:
      params = json.loads(req.body)
    params = _remove_none_and_sort_params(params)

    # urlparse returns ParseResult object which is an iterable object with 6 elements,
    # ['scheme', 'netloc', 'path', 'params', 'query', 'fragment'].
    # Set params, query, and fragment to empty string, only keep the first 3.
    parse_result = urllib.parse.urlparse(req.url)
    url = urllib.parse.ParseResult(
        scheme=parse_result.scheme,
        netloc=parse_result.netloc,
        path=parse_result.path,
        params='',
        query='',
        fragment='',
    ).geturl()
    headers = self._sign_header(req.method, url, params)
    req.headers.update(headers)
    return req


class FcoinSyncAuth(object):
  def __init__(self, key_file, timeout=10):
    self._timeout = timeout
    self._rate_limit = RateLimiter(100, 10.)
    self._auth = FcoinAuth(key_file)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, http_method, endpoint, params):
    url = FCOIN_API_URL + endpoint
    try:
      if http_method == 'GET':
        response = requests.get(url,
                                params=params,
                                json=params,
                                auth=self._auth,
                                timeout=self._timeout)
      elif http_method == 'POST':
        response = requests.post(url, json=params, auth=self._auth, timeout=self._timeout)
      else:
        raise ValueError('Invalid http method %s' % http_method)
      return response
    except requests.exceptions.RequestException as e:
      self._logger.exception('RequestException: %s', e)
      raise
    except json.JSONDecodeError as e:
      self._logger.exception('JsonDecodeException: %s\n %s', response.content, e)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def query_nocheck(self, http_method, endpoint, params=None):
    req_timestamp = time.time()
    response = self.auth_request(http_method, endpoint, params)
    res_timestamp = time.time()
    if response.status_code == 200:
      update = rcb.RestUpdate(req_timestamp,
                              res_timestamp,
                              response.json(),
                              status=response.status_code)
    else:
      update = rcb.RestUpdate(req_timestamp,
                              res_timestamp,
                              response.text,
                              status=response.status_code)
    return update

  def query(self, http_method, endpoint, params=None, disable_rate_limit=False):
    assert http_method in ('GET', 'POST'), http_method
    if not disable_rate_limit:
      while not self._rate_limit.check():
        time.sleep(0.1)
    else:
      self._rate_limit.check()
    return self.query_nocheck(http_method, endpoint, params=params)


class FcoinAsyncAuth(object):
  def __init__(self, key_file, timeout=10, max_connections=8):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = FcoinAuth(key_file)

  def query(self, http_method, endpoint, params=None):
    assert http_method in ('GET', 'POST'), http_method
    url = FCOIN_API_URL + endpoint
    if http_method == 'GET':
      future = self._http_client.get(url,
                                     params=params,
                                     json=params,
                                     auth=self._auth,
                                     timeout=self._timeout)
    elif http_method == 'POST':
      future = self._http_client.post(url, json=params, auth=self._auth, timeout=self._timeout)
    else:
      raise ValueError('Invalid http method %s' % http_method)
    return future


class FcoinNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      timeout=10,
      max_connections=8,
      sync=True,
  ):
    if sync:
      self._auth_queryer = FcoinSyncAuth(key_file, timeout=timeout)
    else:
      self._auth_queryer = FcoinAsyncAuth(key_file,
                                          timeout=timeout,
                                          max_connections=max_connections)

  def _query(self, http_method, url, params=None):
    return self._auth_queryer.query(http_method, url, params=params)

  def query_GET_accounts_balance(self):
    url = 'accounts/balance'
    update = self._query('GET', url)
    return update

  # create order
  def query_POST_orders(self, symbol, order_side, order_type, price, amount):
    assert order_side in ('buy', 'sell'), order_side
    assert order_type == 'limit', order_type
    assert float(price) > 0, price
    assert float(amount) > 0, amount
    url = 'orders'
    params = {
        'symbol': symbol,
        'side': order_side,
        'type': order_type,
        'price': price,
        'amount': amount,
    }
    update = self._query('POST', url, params)
    return update

  # query order
  def query_GET_orders(self, symbol, states='submitted', before=None, after=None, limit=None):
    assert states in fcoin_order_status_list, states
    url = 'orders'
    params = {
        'symbol': symbol,
        'states': states,
        'before': before,
        'after': after,
        'limit': limit,
    }
    update = self._query('GET', url, params)
    return update

  # query specific order
  def query_GET_orders_by_order_id(self, order_id):
    url = 'orders/{order_id}'
    url = url.format(order_id=order_id)
    update = self._query('GET', url)
    return update

  # cancel order
  def query_POST_orders_cancel(self, order_id):
    url = 'orders/{order_id}/submit-cancel'
    url = url.format(order_id=order_id)
    update = self._query('POST', url)
    return update

  # query fills
  def query_GET_match_results(self, order_id):
    url = 'orders/{order_id}/match-results'
    url = url.format(order_id=order_id)
    update = self._query('GET', url)
    return update
